/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package hhf.flex.ascreator;

import hhf.flex.annotations.entities.FlexChildren;
import hhf.flex.annotations.entities.FlexId;
import hhf.flex.annotations.entities.FlexSeeAlso;
import hhf.flex.annotations.entities.FlexTransient;
import hhf.flex.ascreator.exceptions.IgnoredClassException;
import hhf.flex.ascreator.exceptions.IllegalClassException;
import hhf.flex.ascreator.exceptions.NoAnnotationClassException;
import hhf.flex.ascreator.exceptions.NoCommentClassException;
import hhf.flex.ascreator.exceptions.NoSuperClassException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.Transient;

/**
 *
 * @author HHFrancois
 */
public class ASClassCreator extends ASCreator {

    protected Class _class;
    protected String idFieldName = null;

    public ASClassCreator(String path, Class clazz, Map<String, Object> options) {
        super(path, options);
        this._class = clazz;
    }

    @Override
    protected String getASPackage() {
        return _class.getPackage().getName();
    }

    @Override
    protected boolean isInterface() {
        return _class.isInterface();
    }

    @Override
    protected String getASClassName() {
        return _class.getSimpleName();
    }

    @Override
    protected String getASClassComment() throws NoCommentClassException {
        throw new NoCommentClassException();
    }

    @Override
    protected String getASSuperClassName() throws NoSuperClassException {
        Class superclass = _class.getSuperclass();
        if (superclass != null && !superclass.equals(Object.class)) {
            ASClassCreator ascc = new ASClassCreator(path, superclass, options);
            ascc.write();
            return superclass.getName();
        }
        throw new NoSuperClassException();
    }

    @Override
    @SuppressWarnings("unchecked")
    protected void createSeeAlsoASClass() {
        if (_class.isAnnotationPresent(FlexSeeAlso.class)) {
            FlexSeeAlso seeAlso = (FlexSeeAlso) _class.getAnnotation(FlexSeeAlso.class);
            for (Class subClass : seeAlso.value()) {
                if (subClass != null && !subClass.equals(Object.class)) {
                    ASClassCreator ascc = new ASClassCreator(path, subClass, options);
                    ascc.write();
                }
            }
        }
    }

    @Override
    protected Collection<String> getASInterfacesName() {
        Collection<String> interfaces = new ArrayList<String>();
        for (Class interf : _class.getInterfaces()) {
            if (!interf.equals(Serializable.class)) {
                ASClassCreator ascc = new ASClassCreator(path, interf, options);
                ascc.write();
                interfaces.add(interf.getName());
            }
        }
        return interfaces;
    }

    @Override
    protected Set<String> getASImports() {
        Set<String> imports = getASDefaultImports();
        getASImportsFromClass(_class, imports);
        for (Method method : _class.getMethods()) {
            if (method.isAnnotationPresent(FlexTransient.class) || Modifier.isStatic(method.getModifiers())) {
                continue;
            }
            getASImportsFromMethod(method, imports);
        }
        return imports;
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Collection<String> getASClassAnnotations() throws NoAnnotationClassException {
        Collection<String> annos = new ArrayList<String>();
        annos.add("[Bindable]");
        annos.add("[Entity]");
        if (this._class.isAnnotationPresent(Table.class)) {
            Table table = (Table) this._class.getAnnotation(Table.class);
            annos.add("[Table(name=\"" + table.name() + "\")]");
        }
        if (isAbstract()) {
            annos.add("[ExcludeClass]");
        }
        annos.add("[RemoteClass(alias=\"" + this._class.getName() + "\")]");
        return annos;
    }

    /**
     * Retourne une liste d'imports par defaut.
     * @return
     */
    protected Set<String> getASDefaultImports() {
        Set<String> imports = new HashSet<String>();
        imports.add("mx.collections.ArrayCollection");
        imports.add("flash.utils.ByteArray");
        imports.add("flash.errors.IllegalOperationError");
        imports.add("flash.utils.getQualifiedClassName");
        imports.add("mx.utils.SHA256");
        return imports;
    }

    @Override
    protected Map<String, String> getASEvents() {
        return null;
    }

    @Override
    protected Set<String> getASNamespaces() {
        return null;
    }

    @Override
    protected void writeASBodyConstructor(final OutputStreamWriter out) throws IOException {
        if (isAbstract()) {
            out.write("\t\t\tif(getQualifiedClassName(this) == \"" + getASClassName() + "\")\n");
            out.write("\t\t\t\tthrow new IllegalOperationError(\"Class " + getASClassName() + " is abstract and must be implemented in concret class.\");\n");
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    protected void writeASFieldsAndMethods(final OutputStreamWriter out) throws IOException {
        writeExtraASMethods(out);
        Method[] methods = _class.getDeclaredMethods(); // methode de la classe public,protected,private
        Class extend = _class.getSuperclass();
        Class[] interfaces = _class.getInterfaces();
        boolean isOverrided = false;
        for (Method method : methods) {
            if (method.isAnnotationPresent(FlexTransient.class) || Modifier.isStatic(method.getModifiers())) {
                continue;
            }
            if (!Modifier.isPublic(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) {
                continue;
            }
            String fieldName = ASCreatorTools.getFieldName(method);
            if (fieldName != null) { // c'est un getter
                String fieldtype = ASCreatorTools.getASType(method.getReturnType(), legacyCollection);
                try {
                    try {
                        Method m = extend.getMethod(method.getName(), method.getParameterTypes());
                        isOverrided = true; // la methode est visible dans la superclass, override
                        // on pourrait se baser sur l'annotation Override, mais java ne l'impose pas. Donc non fiable.
                    } catch (Exception ex) {
                        isOverrided = false; // pas exposé sur la superclasse
						/* En faite pour flex une methode d'un interface n'est pas override dans la classe concrete */
//						for(Class interfa : interfaces) { // la methode n'est pas dans la superclass, elle est peut etre dans ces interfaces
//							try {
//								interfa.getMethod(method.getName(), method.getParameterTypes());
//								isOverrided = true; // la methode est visible dans cet interface, override
//								break;
//							} catch (Exception ex1) {
//							}
//						}
                    }
                    if (!isInterface()) { // c'est une classe concrete
                        Field field = getField(_class, fieldName);
                        // flextransient : on veut pas l'envoyer à Flex
                        // si le field est transient vis à vis de la serialisation, on l'ignore
                        if (!field.isAnnotationPresent(FlexTransient.class) && !Modifier.isTransient(field.getModifiers())) {
                            // sinon on insert un field et ses getter setter
                            //													isInterface, isAbstract, isOverride
                            insertASField(out, fieldName, fieldtype, false, false, isOverrided, getASAnnotations(fieldName, method, field));
                            if (field.isAnnotationPresent(FlexChildren.class) || method.isAnnotationPresent(FlexChildren.class)) {
                                insertASChildreField(out, fieldName, fieldtype);
                            }
                        }
                    } else { // c'est une classe interface
                        // on ne met que la signature des getter setter
                        insertASFieldAccessors(out, fieldName, fieldtype, true, false, isOverrided, getASAnnotations(fieldName, method, null));
                    }
                } catch (NoSuchFieldException nsfe) { // la methode est surement abstract (donc la classe aussi) car on a pas trouvé le Field
                    if (Modifier.isAbstract(method.getModifiers())) { // la methode est abstract donc insertion seulement des getter setter
                        // avec une exception leve dans le corp des getter setter
                        if (!isOverrided) { // mais elle est pas hérité
                            insertASFieldAccessors(out, fieldName, fieldtype, false, true, false, getASAnnotations(fieldName, method, null));
                        } // sinon elle existait déjà dans la superclasse, donc on ignore
                    } // sinon, c'est surement une methode utilitaire, donc on l'ignore
                }
            }
        }
        if (!isInterface()) {
            insertEqualsMethod(out);
            insertToStringMethod(out);
            insertHashCodeGetter(out);
        }
    }

    /**
     * Crée une methode toString
     * @param out
     * @throws IOException
     */
    private void insertToStringMethod(final OutputStreamWriter out) throws IOException {
        try {
            getASSuperClassName();

        } catch (NoSuperClassException ex) {
            out.write("\t\t[Transient]\n");
            out.write("\t\tpublic function toString():String {\n");
            out.write("\t\t\treturn getQualifiedClassName(this)");
            if (idFieldName != null) {
                out.write("+\"[" + idFieldName + "=\"+this." + idFieldName + "+\"]\";\n");
            } else {
                out.write(";\n");
            }
            out.write("\t\t}\n");
        }
    }

    /**
     * Crée une methode equals
     * @param out
     * @throws IOException
     */
    private void insertEqualsMethod(final OutputStreamWriter out) throws IOException {
        if (idFieldName != null) {
            try {
                getASSuperClassName();
            } catch (NoSuperClassException ex) {
                out.write("\t\t[Transient]\n");
                out.write("\t\tpublic function equals(obj:Object):Boolean {\n");
                out.write("\t\t\tif(this." + idFieldName + " || obj." + idFieldName + ") {\n");
                out.write("\t\t\t\treturn this." + idFieldName + " == obj." + idFieldName + ";\n");
                out.write("\t\t\t}\n");
                out.write("\t\t\treturn false;\n");
                out.write("\t\t}\n");
            }
        }
    }

    /**
     * Crée un attribut en lecture seule retournant le hash de l'objet : hashCode
     * @param out
     * @throws IOException
     */
    private void insertHashCodeGetter(final OutputStreamWriter out) throws IOException {
        try {
            getASSuperClassName();
        } catch (NoSuperClassException ex) {

            out.write("\t\t[Transient]\n");
            out.write("\t\tpublic function get hashCode():String {\n");
            out.write("\t\t\tvar hash:String = \"\";\n");
            out.write("\t\t\tvar b:ByteArray = new ByteArray();\n");
            out.write("\t\t\tb.writeObject(this);\n");
            out.write("\t\t\tb.position = 0;\n");
            out.write("\t\t\thash = SHA256.computeDigest(b);\n");
            out.write("\t\t\treturn hash;\n");
            out.write("\t\t}\n");
        }
    }

    @Override
    protected void checkClass() throws IllegalClassException, IgnoredClassException {
        if (_class.equals(Serializable.class)) {
            throw new IgnoredClassException();
        }
        if (_class.equals(Exception.class)) {
            throw new IgnoredClassException();
        }
        if (!_class.isInterface() && !Serializable.class.isAssignableFrom(_class)) {
            throw new IllegalClassException(_class.getName() + " must implement Serializable interface.");
        }
    }

    /**
     * Si on veut rajouter des methodes personnalisées, c'est ici
     * @param out
     * @throws IOException
     */
    protected void writeExtraASMethods(final OutputStreamWriter out) throws IOException {
    }

    /**
     * Retourne si la classe est abstraite
     * @return
     */
    protected boolean isAbstract() {
        int modifiers = _class.getModifiers();
        return Modifier.isAbstract(modifiers) && !isInterface();
    }

    /**
     * Calcul l'annotation éventuel Inspectable, correspondant au énumeration de valeurs 
     * @param returnType
     * @param asAnnotations
     */
    private void computeInspectableValues(Class returnType, Set<String> asAnnotations) {
        if (returnType.isEnum()) {
            Object[] constants = returnType.getEnumConstants();
            if (constants.length > 0) {
                String inspectable = "[Inspectable(enumeration=\"";
                for (int i = 0; i < constants.length;) {
                    Enum enumC = (Enum) constants[i++];
                    inspectable += enumC.name();
                    if (i < constants.length) {
                        inspectable += ",";
                    } else {
                        inspectable += "\", ";
                    }
                }
                inspectable = inspectable + "defaultValue=\"" + constants[0] + "\", category=\"General\", type=\"" + returnType.getName() + "\")]";
                asAnnotations.add(inspectable);
            }
        }
    }

    /**
     * Calcul l'annotation eventuelle ArrayElementType
     * Valable pour les collections, et les Maps
     * @param returnType
     * @param method
     * @param asAnnotations
     */
    private void computeArrayElementType(Class returnType, Method method, Set<String> asAnnotations) {
        String collectionOf = null;
        if (Collection.class.isAssignableFrom(returnType) || Map.class.isAssignableFrom(returnType)) { // si c'est une collection ou une map, determine de quoi est une collection
            Type genType = method.getGenericReturnType();
            collectionOf = getASArrayElementType(genType);  // c'est une Map ou une Collection
            if (collectionOf != null) {
                asAnnotations.add("[ArrayElementType(\"" + collectionOf + "\")]"); // ceci permet entre autre d'inclure la classe dans la compilation
            }
        }
    }

    /**
     * Retourne les annotations java au format actionScriptt
     * @param field
     * @param fieldType
     * @return
     */
    private Collection<String> getASAnnotations(String fieldName, Method method, Field field) {
        Set<String> asAnnotations = new HashSet<String>();
        Class returnType = method.getReturnType();
        computeInspectableValues(returnType, asAnnotations);
        computeArrayElementType(returnType, method, asAnnotations);

        boolean jpaAnno = false;
        // annotation sur les fields
        if (field != null) {
            Annotation[] annotations = field.getDeclaredAnnotations();
            for (Annotation annotation : annotations) {
                String asAnno = getASAnnotationFromJPAAnnotation(fieldName, annotation);
                if (asAnno != null) {
                    jpaAnno = true;
                    asAnnotations.add(asAnno);
                }

            }
        }
        // annotation sur les methodes
        if (method != null) {
            Annotation[] annotations = method.getDeclaredAnnotations();
            for (Annotation annotation : annotations) {
                String asAnno = getASAnnotationFromJPAAnnotation(fieldName, annotation);
                if (asAnno != null) {
                    jpaAnno = true;
                    asAnnotations.add(asAnno);
                }
            }
        }

        if (!jpaAnno) {
            asAnnotations.add("[Transient]");
            System.out.println("ASClassCreator : Cet attribut " + method.getDeclaringClass().getSimpleName() + "." + fieldName + " ne possede pas d'annotation JPA, il sera annoté [Transient].\nMême si l'objet n'est pas une entité JPA, celle ci peut être annoté par les annotations JPA (sauf l'annotation Entity).");
        }

        return asAnnotations;
    }

    /**
     * Retourne l'annotation JPA java au format actionScript
     * @param anno
     * @param fieldType
     * @return
     */
    private String getASAnnotationFromJPAAnnotation(String fieldName, Annotation anno) {
        String result = null;
        Class<? extends Annotation> annotation = anno.annotationType();
        if (annotation.getPackage().getName().equals("javax.persistence")) {
            if (annotation.equals(Id.class)) {
                result = "[Id(strategy=\"MANUAL\")]";
                if (idFieldName == null) {
                    idFieldName = fieldName; // sert pour construire la methode equals
                }
            } else if (annotation.equals(FlexId.class)) {
                result = "[FlexId(strategy=\"MANUAL\")]";
                idFieldName = fieldName;
            } else if (annotation.equals(GeneratedValue.class)) {
                GeneratedValue a = (GeneratedValue) anno;
            } else if (annotation.equals(OrderBy.class)) {
                OrderBy a = (OrderBy) anno;
                result = "[OrderBy(\"value=" + a.value() + "\")]";
            } else if (annotation.equals(ManyToMany.class)) {
                ManyToMany a = (ManyToMany) anno;
                result = "[ManyToMany(fetch=\"" + a.fetch().name() + "\", cascade=\"PERSIST,MERGE\"";
                if (!"".equals(a.mappedBy())) {
                    result += ", mappedBy=\"" + a.mappedBy() + "\"";
                }
                result += ")]";
            } else if (annotation.equals(OneToMany.class)) {
                OneToMany a = (OneToMany) anno;
                result = "[OneToMany(fetch=\"" + a.fetch().name() + "\", cascade=\"ALL\"";
                if (!"".equals(a.mappedBy())) {
                    result += ", mappedBy=\"" + a.mappedBy() + "\"";
                }
                result += ")]";
            } else if (annotation.equals(ManyToOne.class)) {
                ManyToOne a = (ManyToOne) anno;
                result = "[ManyToOne(fetch=\"" + a.fetch().name() + "\", cascade=\"PERSIST,MERGE\")]";
            } else if (annotation.equals(OneToOne.class)) {
                OneToOne a = (OneToOne) anno;
                result = "[OneToOne(fetch=\"" + a.fetch().name() + "\", cascade=\"ALL\"";
                if (!"".equals(a.mappedBy())) {
                    result += ", mappedBy=\"" + a.mappedBy() + "\"";
                }
                result += ")]";
            } else if (annotation.equals(Lob.class)) {
                Lob a = (Lob) anno;
                result = "[Lob]";
            } else if (annotation.equals(ElementCollection.class)) {
                ElementCollection a = (ElementCollection) anno;
                result = "[ElementCollection(fetch=\"" + a.fetch().name() + "\")]";
            } else if (annotation.equals(Basic.class)) {
                Basic a = (Basic) anno;
                result = "[Basic]";
            } else if (annotation.equals(Temporal.class)) {
                Temporal a = (Temporal) anno;
                result = "[Temporal(value=" + a.value().name() + ")]";
            } else if (annotation.equals(Column.class)) {
                Column a = (Column) anno;
                result = "[Column(name=\"" + a.name() + "\")]";
            } else if (annotation.equals(Transient.class)) {
                // a réflechir car peut etre ne veut on pas le persister en base coté métier, mais peut etre du coté client oui
                result = "[Basic]";
            } else {
                System.out.println("ASClassCreator Annotation JPA non pris en charge : " + annotation.getName());
            }
        }
        return result;
    }
}
